有 Jenkins、有 Gitlab、
有 Web Portal 又有給 Web Portal 部署的 EC2,
看來萬事俱備只欠東風,
而我們今天終於要把整串持續整合和持續部署串起來,
持續整合當然是用 Jenkins,
持續部署的部分我們先用 AWS CodeDeploy 建置,
透過撰寫 Jenkinsfile,
建立整條流水線,
從程式碼建置成品上 S3,
最後使用 AWS CodeDeploy 部署到 EC2。
本次我們要要使用 CodeDeploy 進行部署
所以我們理所當然要建立一個 CodeDeploy 的實例
使用 CodeDeploy 部署到 EC2
會使用到 EC2 上綁定的 IAM Role
沒有綁定 IAM Role 或是沒有相對應的權限
則沒有權限存取為 EC2 部署程式
在 Jenkins CI 的過程中
我們會將現在的版本製作一份放到 S3 上
所以需要多創建一個 Bucket 存放
將資料往 AWS S3 上丟會跑一個有 aws-cli 的 Docker
(不會在 Jenkins Server 上裝 aws-cli)
因此也需要多創建一個有「上傳到 S3」和「CodeDeploy CreateRelease」權限的 IAM
雖然不會在 Jenkins 上安裝 aws-cli
但是沒有另外創建 agent
Pipeline 中的每個步驟都會在 Jenkins Server 上跑
所以需要在 Jenkins Server 上安裝 Docker
這部分會在下面多做描述
前幾天我們建立了一個 S3 的 bucket 給 terraform 存放 tfstate 使用
為了方便分類
所以我們也另外開了一個 Bucket
給 CI/CD 的時候使用
可以在每次 push 的時候
就會 Gitlab 就會透過 webhook 自動 trigger Jenkins
在建置過程就將這次的修改打包一份上 S3 存放
resource "aws_s3_bucket" "artifactory" {
bucket = "ithome-ironman-markmew-jenkins"
acl = "private"
tags = {
Name = "Jenkins Artifactory"
Creator = "Terraform"
}
versioning {
enabled = true
}
}
AWS CodeDeploy 是全受管部署服務
可自動將軟體部署到各種運算服務
包括 Amazon EC2、AWS Fargate、AWS Lambda 和現場部署伺服器
CodeDeply 在建立部署的時候
只支援 S3 和 Github 兩種來源
這也是我們剛剛要建立一個 Bucket 的原因
要將成品放到 S3 上
在 CD 時,再從 S3 抓取封存的檔案進行部署
建立給 IAM 使用的 Role
在撰寫 Terraform 的時候
其實不只是 Role 而已
它還是 instance profile
需要多建立 aws_iam_instance_profile
才能跟 ec2 做綁定
權限的部分我們要綁定 CodeDeploy 和 S3 ReadOnly 的權限
沒有設定 S3 的權限
會造成部署的時候下載 Bundle 下載不下來
resource "aws_iam_instance_profile" "ec2_profile" {
name = "ec2-profile"
role = aws_iam_role.ec2_role.name
}
resource "aws_iam_role" "ec2_role" {
name = "ec2-role"
path = "/"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": [
"codedeploy.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_iam_role_policy_attachment" "ec2_role_codedeploy_role" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole"
role = aws_iam_role.ec2_role.name
}
resource "aws_iam_role_policy_attachment" "ec2_role_s3_readonly" {
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
role = aws_iam_role.ec2_role.name
}
剛剛建立好的 iam role 要連接到昨天建立給 portal 使用的 EC2
在 main.tf 中增加 iam_instance_profile
main.tf
resource "aws_instance" "ithome_ironman_portla" {
.
.
.
hibernation = false
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
.
.
.
}
建立 CodeDeploy 的程式碼就比較容易理解
需要先建立一個部署的 app
然後要建立部署的目標群組
這目標群組是用 tag 來做選擇
tag 在 aws 上主要用途也是如此
除了一般標記讓你容易辨別以外
有些服務會需要特別下特定的 tag
才能夠被辨識出來
resource "aws_codedeploy_app" "portal" {
name = "ithome-ironman-portal"
}
resource "aws_codedeploy_deployment_group" "portal" {
app_name = aws_codedeploy_app.portal.name
deployment_group_name = "ithome-ironman-portal"
service_role_arn = aws_iam_role.ec2_role.arn
ec2_tag_set {
ec2_tag_filter {
key = "Name"
type = "KEY_AND_VALUE"
value = "ithome ironman 2021 portal"
}
}
auto_rollback_configuration {
enabled = true
events = ["DEPLOYMENT_FAILURE"]
}
}
在 CI 的時候
Jenkins 會先將現在的 Code 打包一份送到S3
這部分不考慮在 Jenkins EC2 上先 config
純粹只是不想在 Jenkins 上裝太多東西而已
此外 Jenkins 相對重要
雖然 config 在 server 上和起 Docker 的同時把 config mount 上去差不多
但是這部分我期望多做一步繞個遠路
萬一 Jenkins 被打
也不會很快的所有資料被看光
也因為不是用 SSH 連到 EC2 Portal
只能使用 aws cli (api) 來呼叫
所以運作上反而相對單純獨立
main.tf
resource "aws_iam_user" "jenkins" {
name = "Jenkins"
path = "/"
tags = {
Name = "Jenkins"
Usage = "Jenkins"
Creator = "Terraform"
}
}
resource "aws_iam_access_key" "jenkins" {
user = aws_iam_user.jenkins.name
}
resource "aws_iam_user_policy" "jenkins_s3_upload" {
name = "JenkinsS3Upload"
user = aws_iam_user.jenkins.name
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::ithome-ironman-markmew-jenkins"
]
}
]
}
EOF
}
resource "aws_iam_user_policy" "jenkins_create_deployment" {
name = "JenkinsCreateDeployment"
user = aws_iam_user.jenkins.name
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "codedeploy:CreateDeployment",
"Effect": "Allow",
"Resource": "arn:aws:codedeploy:ap-northeast-1:776212102166:deploymentgroup:ithome-ironman-portal/ithome-ironman-portal"
},
{
"Action": [
"codedeploy:GetDeploymentInstance",
"codedeploy:GetDeploymentGroup",
"codedeploy:ListDeploymentInstances",
"codedeploy:GetDeploymentConfig",
"codedeploy:ListTagsForResource",
"codedeploy:GetDeployment",
"codedeploy:ListDeployments"
],
"Effect": "Allow",
"Resource": [
"arn:aws:codedeploy:ap-northeast-1:776212102166:deploymentgroup:ithome-ironman-portal/ithome-ironman-portal",
"arn:aws:codedeploy:*:776212102166:deploymentconfig:*"
]
},
{
"Action": "codedeploy:RegisterApplicationRevision",
"Effect": "Allow",
"Resource": "arn:aws:codedeploy:ap-northeast-1:776212102166:application:ithome-ironman-portal"
}
]
}
EOF
}
outputs.tf
output "jenkins_access_key_id" {
description = "The access key ID"
value = aws_iam_access_key.jenkins.id
}
output "jenkins_secret_token" {
description = "Decrypt access secret key command"
value = aws_iam_access_key.jenkins.secret
sensitive = true
}
terraform apply
因為 secret token 是機密資訊
所以在 terraform apply 的時候會隱藏
需要再多輸入 output 指令才能輸出 secret token
在使用 terraform 建立存取金鑰的時候
這些資料就會紀錄在 tfstate 裏面
基於上次所提到的 pem key 以及這次 secret token
我相信更可以理解為什麼 tfstate 不應該進版控
這樣等同是把帳密資訊一併進版控
terraform output jenkins_secret_token
恩,對
你沒看錯
要在 EC2 上安裝 CodeDeploy Agent
雖然步驟很簡單
但是沒有裝 Agent 會部署不上去
sudo apt install ruby-full
cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-1.s3.ap-northeast-1.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto > /tmp/logfile
sudo ./install auto -v releases/codedeploy-agent-###.deb > /tmp/logfile
AWS 上持續部署所需要的基礎設施先告一段落
但是我們還沒在 Jenkins 上建立 Pipeline
之前有提到我們不打算在 Jenkins 的 EC2 上安裝 aws-cli
取而代之的則是起 Docker 來執行 aws-cli
剛剛為 Jenkins 建立的 IAM User
在 Pipeline 中除了會 mount IAM User 的設定
將建置結果上傳到 S3 以外
也會呼叫 CodeDeploy 建立一個新的版本
透過 CodeDeploy 將服務部署到 EC2
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt update
sudo apt-get install docker-ce
docker --version
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker jenkins
sudo usermod -aG root jenkins
sudo usermod -aG ubuntu jenkins
sudo chmod 644 /var/run/docker.sock
sudo chown jenkins:docker /var/run/docker.sock
到「管理 Jenkins」中的「管理外掛程式」
選擇「Docker Commons」和「Docker Pipeline」進行安裝
早期大家在做 CI/CD 時
都會在 VM 上藏 SSH Key
若是沒有控管好權限
Jenkins 又沒有定期修補漏洞
在 Jenkins 被攻破後就有可能全部淪陷
其實搬上雲端也是一樣的道理
盡可能給予適當的權限即可
雖然我們在 Jenkins Server 上藏了 IAM User 的 Key 和 Token
但是權限上我們只限制上傳到 S3
以及 Create Release 的權限而已
sudo mkdir /usr/local/src/aws_docker_file
sudo mkdir /usr/local/src/aws_docker_file/.aws
sudo touch /usr/local/src/aws_docker_file/.aws/config
sudo touch /usr/local/src/aws_docker_file/.aws/credentials
不是使用 aws config 去設定
是實際創建檔案
為了起 Docker 時才能夠把 IAM user mount 上去
/usr/local/src/aws_docker_file/.aws/config
[default]
region = ap-northeast-1
output = json
將 terraform 做出來的 key id 和 secret 填入
/usr/local/src/aws_docker_file/.aws/credentials
[default]
aws_access_key_id = 你的 KEY
aws_secret_access_key = 你的 Secret
credentialsId 前幾天有在 Jenkins 裡面添加
這個直接拿來用就可以了
withDockerContainer 顧名思義是起一個 container
並在內部執行相關指令
因為我們只是個初始化專案
所有跑測試一定會通過
「Run Test」這部分是為了以後可能有撰寫測試而留的
「Archieve Project」是使用 git 的指令將程式碼另外壓縮
會另外下這個指令
也是因為 .git 裡面包含太多資訊
如果不是另外 export 沒有 .git 的乾淨版本
會更容易被試出系統的漏洞
pipeline {
agent any
stages {
stage('Git Checkout') {
steps {
sh 'pwd'
sh 'ls -a'
retry(3) {
dir('ithome-ironman') {
git branch: 'develop',
credentialsId: '你的Credentails',
url: 'git@你的IP或HOST:ithome-ironman-2021/portal.git'
}
}
}
}
stage('Run Test') {
steps {
echo 'Run Python Unittest ...'
dir('ithome-ironman') {
script {
withDockerContainer(image: 'python:3.7.10-buster', args: '-u root:root') {
sh """
apt-get install libpq-dev
pip install --user -r requirements.txt
python manage.py test
rm -rf __pycache__
rm -rf */__pycache__
rm -rf */*.pyc
"""
}
}
}
}
}
stage('Archieve Project') {
steps {
echo 'Archieve...'
dir('ithome-ironman') {
sh 'git archive --format=tar.gz --output ./portal.tar.gz HEAD'
}
}
}
stage('Upload to S3') {
steps {
echo 'Upload...'
dir('ithome-ironman') {
sh "docker run --rm -v ${WORKSPACE}/ithome-ironman:/app -v /usr/local/src/aws_docker_file/.aws:/root/.aws mikesir87/aws-cli aws s3 cp /app/portal.tar.gz s3://ithome-ironman-markmew-jenkins/portal/stage/portal-${env.BUILD_ID}.tar.gz"
}
}
}
stage('Deploy') {
steps {
echo 'Deploy ...'
dir('ithome-ironman') {
sh "docker run --rm -v /usr/local/src/aws_docker_file/.aws:/root/.aws mikesir87/aws-cli aws deploy create-deployment --application-name ithome-ironman-portal --deployment-config-name CodeDeployDefault.OneAtATime --deployment-group-name ithome-ironman-portal --s3-location bucket=ithome-ironman-markmew-jenkins, bundleType=tgz, key=portal/stage/portal-${env.BUILD_ID}.tar.gz"
}
}
}
}
}
裝完 Agent
還是要在專案底下
新增一些設定檔
appspec.yaml 在撰寫時要注意
如果檔案不存在會部署失敗
之前手動部署上去的檔案要先刪掉,不然也會部署失敗
至於 version 好像也只能是 0.0
有試著改成 0.1 或是 1.0 也都會失敗
scripts/start_server
source /var/www/venv/portal/bin/activate/bin/activate
pip install -r /var/www/portal/requirements.txt
service apache2 start
scripts/stop_server
service apache2 stop
scripts/install_dependencies
#!/bin/bash
apt-get update
apt-get install pip3
apt-get install python3 python3-virtualenv python3-pip libpq-dev python-dev
cd /var/www/venv
virtualenv portal
source portal/bin/activate
pip install -r /var/www/portal/requirements.txt
appspec.yml
version: 0.0
os: linux
files:
- source: /manage.py
destination: /var/www/portal
- source: /requirements.txt
destination: /var/www/portal
- source: /portal/
destination: /var/www/portal/portal
permissions:
- object: /var/www/portal/manage.py
owner: ubuntu
mode: 644
type:
- file
hooks:
AfterInstall:
- location: scripts/install_dependencies
timeout: 300
runas: root
- location: scripts/start_server
timeout: 300
runas: root
ApplicationStop:
- location: scripts/stop_server
timeout: 300
runas: root
今天的資訊量有點多又有點雜
AWS CodeDeploy 只允許 Github 和 S3 兩個來源
所以我們需要先創建一個 Bucket
將 CI 過程中的 Code 打包一份上 S3
為了建立 AWS CodeDeploy
需要幫 EC2 建立 iam profile 並綁定 AWS CodeDeploy 和 S3 讀取權限
即使如此還是需要在 EC2 上裝 CodeDeploy Agent
這樣才能在 Pipeline 的最後 Create Deployment
CodeDeploy 進入 EC2 進行部署的時候才能順利去 S3 抓資料
在 Jenkins 執行 CI/CD 的過程
需要用到 Docker
所以需要在 Jenkins 的 EC2 裝 Docker
以及在 Jenkins 上裝一些套件
我其實不太想要說什麼了
大家照著步驟做當然可以部署在 on-premise 的機械上
但是專案內新增部署流程不說
IAM 的權限設定綁這麼死
網路 inbound/outbound 綁這麼死
CodeDeploy 還要在 on-premise 裝 Agent
難怪大家都不太愛寫 AWS Cloud 的教學文
除了貴又麻煩以外
還要把 AWS 的每份文件都翻過好幾輪
才知道原來有些文章和細節真的要仔細看
不然做不出來真的會想要 Grant Administrator 權限給它就好了
參考資料: